package cn.gov.msa.aop;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import cn.gov.msa.util.RedisSynLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
 
/**
 * 锁的切面编程<br/>
 * 针对添加@RedisSynLock 注解的方法进行加锁
 * 
 * @see cn.gov.msa.util.RedisSynLock
 * 
 * @author junxin
 *
 */
@Aspect
@Component
public class RedisLockAspect {
	@Autowired
	@Qualifier("redisTemplate")
	private RedisTemplate<String, Object> redisTemplate;
	
	Logger logger = Logger.getLogger(RedisLockAspect.class);
	
	public static final String UNLOCK_LUA;
	 
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }
	
	@Around("execution(* cn.gov.msa.service.impl.ScheduleServiceImpl.*(..)) && @annotation(cn.gov.msa.util.RedisSynLock)")
	public Object tryLock(ProceedingJoinPoint pjp) throws Throwable {
		RedisSynLock lockInfo = getLockInfo(pjp);
		if (lockInfo == null) {
			throw new IllegalArgumentException("配置参数错误");
		}
 
		String synKey = getSynKey(pjp, lockInfo.synKey());
		if (synKey == null || "".equals(synKey)) {
			throw new IllegalArgumentException("配置参数synKey错误");
		}
 
		String uuid = UUID.randomUUID().toString();
		boolean lock = false;
		Object obj = null;
		try {
			lock = setLock(synKey, uuid, lockInfo.expire());
			logger.info("是否获取到了锁" + lock);
			// 如果获取到了锁，执行方法
			if (lock) {
				obj = pjp.proceed();
			}
 
		} catch (Exception e) {
			throw e;
		} finally {
			// 如果获取到了锁，释放锁
			if (lock) {
				releaseLock(synKey, uuid);
			}
		}
		return obj;
	}
 
	/**
	 * redis key的拼写规则为 "RedisSynLock_" + synKey
	 * 
	 */
	private String getSynKey(ProceedingJoinPoint pjp, String synKey) {
		try {
			synKey = "RedisSynLock_" + synKey;
//			Object[] args = pjp.getArgs();
//			if (args != null && args.length > 0) {
//				MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
//				Annotation[][] paramAnnotationArrays = methodSignature.getMethod().getParameterAnnotations();
// 
//				SortedMap<Integer, String> keys = new TreeMap<Integer, String>();
// 
//				for (int ix = 0; ix < paramAnnotationArrays.length; ix++) {
//					RedisSynLock p4jSynKey = getAnnotation(RedisSynLock.class, paramAnnotationArrays[ix]);
//					if (p4jSynKey != null) {
//						Object arg = args[ix];
//						if (arg != null) {
//							keys.put(p4jSynKey.index(), arg.toString());
//						}
//					}
//				}
// 
//				if (keys != null && keys.size() > 0) {
//					for (String key : keys.values()) {
//						synKey = synKey + key;
//					}
//				}
//			}
 
			return synKey;
		} catch (Exception e) {
			logger.error("获取synKey失败.", e);
		}
		return null;
	}
 
	/**
	 * 获取RedisLock注解信息
	 */
	private RedisSynLock getLockInfo(ProceedingJoinPoint pjp) {
		try {
			MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
			Method method = methodSignature.getMethod();
			RedisSynLock lockInfo = method.getAnnotation(RedisSynLock.class);
			return lockInfo;
		} catch (Exception e) {
			logger.error("获取RedisLock注解信息失败.", e);
		}
		return null;
	}

    /**
     * 获取 Redis 锁
     *
     * @param key         锁名称
     * @param value       锁对应的值
     * @param expire      锁过期时间
     * @return
     */
	public boolean setLock(final String key, final String value, final long expire) {
        try {
            RedisCallback<String> callback = new RedisCallback<String>() {
				@Override
				public String doInRedis(RedisConnection connection) throws DataAccessException {
				    JedisCommands commands = (JedisCommands) connection.getNativeConnection();
				    return commands.set(key, value, "NX", "PX", expire);
				}
			};
            String result = redisTemplate.execute(callback);
 
            return !StringUtils.isEmpty(result);
        } catch (Exception e) {
            logger.error("获取 Redis 锁发生异常", e);
        }
        return false;
    }
 
	/**
     * 释放锁
     * @param key 锁
     * @param value 锁对应的值
     * @return
     */
    public boolean releaseLock(String key,String value) {
        // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
        try {
            final List<String> keys = new ArrayList<>(1);
            keys.add(key);
            final List<String> args = new ArrayList<>(1);
            args.add(value);
 
            // 使用lua脚本删除redis中匹配value的key，可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中，集群模式直接抛出不支持执行脚本的异常，所以只能拿到原redis的connection来执行脚本
            RedisCallback<Long> callback = new RedisCallback<Long>() {
				@Override
				public Long doInRedis(RedisConnection connection) throws DataAccessException {
				    Object nativeConnection = connection.getNativeConnection();
				    // 集群模式和单机模式虽然执行脚本的方法一样，但是没有共同的接口，所以只能分开执行
				    // 集群模式
				    if (nativeConnection instanceof JedisCluster) {
				        return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
				    }
 
				    // 单机模式
				    else if (nativeConnection instanceof Jedis) {
				        return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
				    }
				    return 0L;
				}
			};
            Long result = redisTemplate.execute(callback);
 
            return result != null && result > 0;
        } catch (Exception e) {
            logger.error("redis释放锁("+key+")发生异常", e);
        } finally {
        	
        }
        return false;
    }
}

